之前的二天已經介紹了要如何使用 Java 啟動多執行緒的程式了,但是沒有考慮到執行緒安全的部份,以下簡單的介紹什麼是執行緒安全?
執行緒安全是指一個 class 的 Instance 裡的共享變數同時被多個執行緒執行,這個共享變數的值是一個可預期的結果而且每次執行的結果都一致的話,這個共享變數就是執行緒安全,而不會因為開啟多執行緒之後破壞了共享變數最後的結果。
因此我們在使用 Java 內建的 class 或是第三方元件如果需要開啟多執行緒一定要去查詢 Java API 文件確保使用的 class 是具有執行緒安全的部份,如果不小心用到執行緒不安全的 class 之後程式有可能就會出現 Bug,有可能是在測試階段不會發現,要等而正式的上線才會發現。通常遇到這種情況 Debug 起來都會非常的累:
另外我們在設計 API 給其他人使用,也要注意到每個變數或是方法是否具有執行緒安全的部份,這些都要在文件上寫清楚才不會讓使用者有誤會而使得使用者開發出來的程式出現 Bug。
舉幾個執行緒不安全的 Java Class:
ArrayList、LinkedList、Map、HashMap、Set、HashSet
以下就使用 ArrayList 寫一個執行緒不安全的 Sample Code:
import java.util.List;
public class ArrayListThreadTest1 implements Runnable {
private List<String> list;
public ArrayListThreadTest1(List<String> list) {
this.list = list;
}
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
Thread.sleep(1000);
this.list.add("a" + i);
}
}catch(Exception e) {
throw new RuntimeException(e);
}
}
}
ArrayListThreadTest1 程式的建構子會收到 list 的 Instance,之後在執行 run 方法時會先模疑一個 IO 操作,這裡簡單的 sleep 1 秒之後,再把 list 放入一個元素的值。
這裡的 list 為共享變數提供給多個執行緒使用,以下為主程式的內容:
import java.util.ArrayList;
import java.util.List;
public class Test {
public static void main(String args[]) throws Exception {
List<String> list = new ArrayList<String>();
Thread thread1 = new Thread(new ArrayListThreadTest1(list));
Thread thread2 = new Thread(new ArrayListThreadTest1(list));
Thread thread3 = new Thread(new ArrayListThreadTest1(list));
thread1.start();
thread2.start();
thread3.start();
thread1.join();
thread2.join();
thread3.join();
System.out.println("list size is " + list.size());
}
}
在主程式會啟動 3 個執行緒並且共用一個 list 的變數,在執行完執行緒程式之後會印出結果,程式的執行結果如下:
list size is 24
我們會發現 list 的 size 為 24 而不是 30,而且每次執行的結果都不一樣,這代表了使用 ArrayList 是執行緒不安全。需要把 list 修改成執行緒安全輸出的結果才會是 30,修改後的程式如下:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Test {
public static void main(String args[]) throws Exception {
List<String> list = Collections.synchronizedList(new ArrayList<String>());
Thread thread1 = new Thread(new ArrayListThreadTest1(list));
Thread thread2 = new Thread(new ArrayListThreadTest1(list));
Thread thread3 = new Thread(new ArrayListThreadTest1(list));
thread1.start();
thread2.start();
thread3.start();
thread1.join();
thread2.join();
thread3.join();
System.out.println("list size is " + list.size());
}
}
主要就是把 ArrayList 修改成呼叫 Collections.synchronizedList 的方式讓共享變數可以執行緒安全,這樣就可以解決 list 變數輸出結果每次都不一致的問題,關於執行緒安全的 Collection 之後都還會再介紹。